﻿Imports Microsoft.VisualBasic

''' <summary>
''' Manages the game's asset folder and supplies actual assets to the AssetBank.
''' </summary>
''' <remarks>
''' </remarks>
Public Class AssetManager
    Private Const DEFAULT_ASSET_FOLDER_NAME As String = "Assets"
    Private Const VALID_AUDIO_EXTENSIONS As String = ".WAV.MP3.MID"
    Private Const VALID_IMAGE_EXTENSIONS As String = ".BMP.PNG.JPG.TIF.TIFF"

    'Protected Friend _CachedResources As New AssetBank(Me)
    Protected Friend _CachedResources As New DictionaryBank(Of String)

    Private _AssetPath As String
    Private _AssetFolderName As String
    ''' <summary>
    ''' The name of the projects Asset folder.  This value is "Assets" by default.
    ''' </summary>
    ''' <value>Sets the name of the Asset folder.</value>
    ''' <returns>Gets the name of the Asset folder.</returns>
    ''' <remarks></remarks>
    Public Property AssetFolderName() As String
        Get
            Return _AssetFolderName
        End Get
        Set(ByVal value As String)
            If value Is Nothing OrElse value.Length = 0 Then
                value = DEFAULT_ASSET_FOLDER_NAME
            End If
            _AssetFolderName = value
            _AssetPath = System.IO.Path.Combine(My.Application.Info.DirectoryPath, _AssetFolderName)
        End Set
    End Property

    Private _GameDataFile As String = String.Empty
    ''' <summary>
    ''' The name of the .cfg data file containing global game data to be loaded when the game starts.
    ''' </summary>
    ''' <value>Sets the name of the data file.</value>
    ''' <returns>Gets the name of the data file.</returns>
    ''' <remarks></remarks>
    Public Property GameDataFile() As String
        Get
            Return _GameDataFile
        End Get
        Set(ByVal value As String)
            If value Is Nothing Then value = String.Empty
            _GameDataFile = value
        End Set
    End Property

    ''' <summary>
    ''' Gets the file path to an audio asset with the specified assetName.
    ''' </summary>
    ''' <param name="assetName">The name of the audio asset whose URI should be returned.</param>
    ''' <returns>A System.URI containing the path to the audio asset.</returns>
    ''' <remarks></remarks>
    Public Function GetAudioAsset(ByVal assetName As String) As Uri
        If _CachedResources.ContainsKey(Of Uri)(assetName) Then
            Return _CachedResources.GetValue(Of Uri)(assetName)
        End If
        Throw New ArgumentException("An asset with the specified name could not be found.")
    End Function

    Public Sub ClearDefinitonFileCache()
        _CachedResources.Clear(Of String())()
    End Sub

    Public Function GetDefinitionFile(assetName As String) As String()
        If _CachedResources.ContainsKey(Of String())(assetName) Then
            Return _CachedResources.GetValue(Of String())(assetName)
        End If
        Dim fileNames() As String = System.IO.Directory.GetFiles(_AssetPath, assetName & ".gdf", IO.SearchOption.AllDirectories)
        If fileNames.Length > 0 Then
            Dim fileName As String = fileNames(0)
            Dim lines() As String = System.IO.File.ReadAllLines(fileName)
            SetDefinitionFile(assetName, lines)
            Return lines
        End If
        Throw New ArgumentException("An asset with the specified name could not be found.")
    End Function

    ''' <summary>
    ''' Gets the Bitmap instance for an image asset with the specified assetName.
    ''' </summary>
    ''' <param name="assetName">The name of the image asset Bitmap to return.</param>
    ''' <returns>The Bitmap for this image asset.</returns>
    ''' <remarks></remarks>
    Public Function GetImageAsset(ByVal assetName As String) As Bitmap
        If _CachedResources.ContainsKey(Of Bitmap)(assetName) Then
            Return _CachedResources.GetValue(Of Bitmap)(assetName)
        End If
        Dim fileNames() As String = System.IO.Directory.GetFiles(_AssetPath, assetName & ".*", IO.SearchOption.AllDirectories)
        If fileNames.Length > 0 Then
            Dim fileName As String = fileNames(0)
            Dim extension As String = System.IO.Path.GetExtension(fileName).ToUpper
            If VALID_IMAGE_EXTENSIONS.Contains(extension) Then
                Dim imageResource As Bitmap = Bitmap.FromFile(fileName)
                SetImageAsset(assetName, imageResource)
                Return imageResource
            End If
            Throw New ArgumentException("An asset with the specified name was found, but is not of the desired type.")
        End If
        Throw New ArgumentException("An asset with the specified name could not be found.")
    End Function

    ''' <summary>
    ''' Gets the boolean value for the flag asset with the specified assetName.
    ''' </summary>
    ''' <param name="assetName">The name of the asset to return.</param>
    ''' <returns>The value of the specified asset.</returns>
    ''' <remarks></remarks>
    Public Function GetFlagAsset(assetName As String) As Boolean
        If _CachedResources.ContainsKey(Of Boolean)(assetName) Then
            Return _CachedResources.GetValue(Of Boolean)(assetName)
        End If
        Throw New ArgumentException("An asset with the specified name could not be found.")
    End Function

    ''' <summary>
    ''' Gets the integer value for the asset with the specified assetName.
    ''' </summary>
    ''' <param name="assetName">The name of the asset to return.</param>
    ''' <returns>The value of the specified asset.</returns>
    ''' <remarks></remarks>
    Public Function GetIntegerAsset(assetName As String) As Integer
        If _CachedResources.ContainsKey(Of Integer)(assetName) Then
            Return _CachedResources.GetValue(Of Integer)(assetName)
        End If
        Throw New ArgumentException("An asset with the specified name could not be found.")
    End Function

    ''' <summary>
    ''' Gets the single value for the asset with the specified assetName.
    ''' </summary>
    ''' <param name="assetName">The name of the asset to return.</param>
    ''' <returns>The value of the specified asset.</returns>
    ''' <remarks></remarks>
    Public Function GetSingleAsset(assetName As String) As Single
        If _CachedResources.ContainsKey(Of Single)(assetName) Then
            Return _CachedResources.GetValue(Of Single)(assetName)
        End If
        Throw New ArgumentException("An asset with the specified name could not be found.")
    End Function

    ''' <summary>
    ''' Gets the string value for the asset with the specified assetName.
    ''' </summary>
    ''' <param name="assetName">The name of the asset to return.</param>
    ''' <returns>The value of the specified asset.</returns>
    ''' <remarks></remarks>
    Public Function GetStringAsset(assetName As String) As String
        If _CachedResources.ContainsKey(Of String)(assetName) Then
            Return _CachedResources.GetValue(Of String)(assetName)
        End If
        Throw New ArgumentException("An asset with the specified name could not be found.")
    End Function

    ''' <summary>
    ''' Gets the value for the asset with the specified assetName.
    ''' </summary>
    ''' <param name="assetName">The name of the asset to return.</param>
    ''' <returns>The value of the specified asset.</returns>
    ''' <remarks></remarks>
    Public Function GetAsset(Of TValue)(assetName As String) As TValue
        If _CachedResources.ContainsKey(Of TValue)(assetName) Then
            Return _CachedResources.GetValue(Of TValue)(assetName)
        End If
        Throw New ArgumentException("An asset with the specified name could not be found.")
    End Function

    Public Sub ModifyIntegerAsset(assetName As String, amount As Integer)
        Dim value = GetIntegerAsset(assetName)
        value += amount
        SetIntegerAsset(assetName, value)
    End Sub

    ''' <summary>
    ''' Adds an audio asset the cache, or replaces the existing if the specified assetName exists.
    ''' </summary>
    ''' <param name="assetName">The name of the asset to add or replace.</param>
    ''' <param name="assetValue">The asset to add or replace.</param>
    ''' <remarks></remarks>
    Public Sub SetAudioAsset(assetName As String, assetValue As Uri)
        _CachedResources.SetValue(Of Uri)(assetName, assetValue)
    End Sub

    Public Sub SetDefinitionFile(assetName As String, assetValue() As String)
        _CachedResources.SetValue(Of String())(assetName, assetValue)
    End Sub

    ''' <summary>
    ''' Adds an image asset the cache, or replaces the existing if the specified assetName exists.
    ''' </summary>
    ''' <param name="assetName">The name of the asset to add or replace.</param>
    ''' <param name="assetValue">The asset to add or replace.</param>
    ''' <remarks></remarks>
    Public Sub SetImageAsset(assetName As String, assetValue As Bitmap)
        _CachedResources.SetValue(Of Bitmap)(assetName, assetValue)
    End Sub

    ''' <summary>
    ''' Adds a flag (boolean) asset the cache, or replaces the existing if the specified assetName exists.
    ''' </summary>
    ''' <param name="assetName">The name of the asset to add or replace.</param>
    ''' <param name="assetValue">The asset to add or replace.</param>
    ''' <remarks></remarks>
    Public Sub SetFlagAsset(assetName As String, assetValue As Boolean)
        _CachedResources.SetValue(Of Boolean)(assetName, assetValue)
    End Sub

    ''' <summary>
    ''' Adds an integer asset the cache, or replaces the existing if the specified assetName exists.
    ''' </summary>
    ''' <param name="assetName">The name of the asset to add or replace.</param>
    ''' <param name="assetValue">The asset to add or replace.</param>
    ''' <remarks></remarks>
    Public Sub SetIntegerAsset(assetName As String, assetValue As Integer)
        _CachedResources.SetValue(Of Integer)(assetName, assetValue)
    End Sub

    ''' <summary>
    ''' Adds a single asset the cache, or replaces the existing if the specified assetName exists.
    ''' </summary>
    ''' <param name="assetName">The name of the asset to add or replace.</param>
    ''' <param name="assetValue">The asset to add or replace.</param>
    ''' <remarks></remarks>
    Public Sub SetSingleAsset(assetName As String, assetValue As Single)
        _CachedResources.SetValue(Of Single)(assetName, assetValue)
    End Sub

    ''' <summary>
    ''' Adds a string asset the cache, or replaces the existing if the specified assetName exists.
    ''' </summary>
    ''' <param name="assetName">The name of the asset to add or replace.</param>
    ''' <param name="assetValue">The asset to add or replace.</param>
    ''' <remarks></remarks>
    Public Sub SetStringAsset(assetName As String, assetValue As String)
        _CachedResources.SetValue(Of String)(assetName, assetValue)
    End Sub

    ''' <summary>
    ''' Adds an asset of type T the cache, or replaces the existing if the specified assetName exists.
    ''' </summary>
    ''' <param name="assetName">The name of the asset to add or replace.</param>
    ''' <param name="assetValue">The asset to add or replace.</param>
    ''' <remarks></remarks>
    Public Sub SetAsset(Of TValue)(assetName As String, assetValue As TValue)
        _CachedResources.SetValue(Of TValue)(assetName, assetValue)
    End Sub

    Public Sub New()
        AssetFolderName = DEFAULT_ASSET_FOLDER_NAME
    End Sub

    Protected Friend Sub LoadInitialAssets()
        Dim fileNames() As String = System.IO.Directory.GetFiles(_AssetPath, "*.*", IO.SearchOption.AllDirectories)
        For Each filename As String In fileNames
            Dim extension As String = System.IO.Path.GetExtension(filename).ToUpper
            If VALID_AUDIO_EXTENSIONS.Contains(extension) Then
                Dim assetName As String = System.IO.Path.GetFileNameWithoutExtension(filename)
                SetAudioAsset(assetName, New Uri(filename))
            End If
        Next
        If _GameDataFile.Length > 0 Then
            fileNames = System.IO.Directory.GetFiles(_AssetPath, _GameDataFile & ".cfg", IO.SearchOption.AllDirectories)
            If fileNames.Length = 1 Then
                Dim fileContents As String = System.IO.File.ReadAllText(fileNames(0))
                ParseConfigFile(fileContents)
            End If
        End If
    End Sub

    Protected Sub ParseConfigFile(fileContents As String)
        Dim result As New DictionaryBank(Of String)
        Dim lines() As String = fileContents.Split(ControlChars.NewLine)
        For index As Integer = 0 To lines.Count - 1
            Dim line As String = lines(index).Trim
            If line.Length > 0 Then
                Dim token As Char = line(0)
                line = line.Substring(1)
                If token = "@"c Then 'string
                    ValidateParseLine(Of String)(line, index, result)
                ElseIf token = "#"c Then 'integer
                    ValidateParseLine(Of Integer)(line, index, result)
                ElseIf token = "$"c Then 'single
                    ValidateParseLine(Of Single)(line, index, result)
                ElseIf token = "%"c Then 'flag
                    ValidateParseLine(Of Boolean)(line, index, result)
                ElseIf Not token = "!"c Then 'comment
                    Throw New ArgumentException("File does not appear to be a valid game configuration file. Invalid line token found at line " & index)
                End If
            End If
        Next
        For Each key In result.GetKeys(Of String)()
            SetStringAsset(key, result.GetValue(Of String)(key))
        Next
        For Each key In result.GetKeys(Of Integer)()
            SetIntegerAsset(key, result.GetValue(Of Integer)(key))
        Next
        For Each key In result.GetKeys(Of Single)()
            SetSingleAsset(key, result.GetValue(Of Single)(key))
        Next
        For Each key In result.GetKeys(Of Boolean)()
            SetFlagAsset(key, result.GetValue(Of Boolean)(key))
        Next
    End Sub

    Protected Sub ValidateParseLine(Of T)(line As String, lineNumber As Integer, results As DictionaryBank(Of String))
        Dim values() As String
        values = line.Split("="c)
        If values.Length = 2 Then
            values(0) = values(0).Trim
            If values(0).Length < 1 Then Throw New ArgumentException("File appears to be a valid game configuration file, however, it contains an error. Missing asset name on line " & lineNumber)
            If results.ContainsKey(Of T)(values(0)) Then Throw New ArgumentException("File appears to be a valid game configuration file, however, it contains an error. Asset name already exists on line " & lineNumber)

            values(1) = values(1).Trim
            If values(1).Length < 1 Then Throw New ArgumentException("File appears to be a valid game configuration file, however, it contains an error. Missing asset value on line " & lineNumber)

            Dim assetValue As T
            If GetType(T) Is GetType(Integer) Then
                Dim asset As Integer
                If Not Integer.TryParse(values(1), asset) Then Throw New ArgumentException("File appears to be a valid game configuration file, however, it contains an error. Invalid integer asset value on line " & lineNumber)
                assetValue = CObj(asset)
            ElseIf GetType(T) Is GetType(Single) Then
                Dim asset As Single
                If Not Single.TryParse(values(1), asset) Then Throw New ArgumentException("File appears to be a valid game configuration file, however, it contains an error. Invalid single asset value on line " & lineNumber)
                assetValue = CObj(asset)
            ElseIf GetType(T) Is GetType(Boolean) Then
                Dim asset As Boolean
                If Not Boolean.TryParse(values(1), asset) Then Throw New ArgumentException("File appears to be a valid game configuration file, however, it contains an error. Invalid flag asset value on line " & lineNumber)
                assetValue = CObj(asset)
            ElseIf Not GetType(T) Is GetType(String) Then
                Throw New ArgumentException("The type T was not recognized. Valid types are String, Integer, Single, and Boolean.")
            Else
                assetValue = CObj(values(1))
            End If

            results.SetValue(Of T)(values(0), assetValue)
        Else
            Throw New ArgumentException("File appears to be a valid game configuration file, however, it contains an error. Missing operator token on line " & lineNumber)
        End If
    End Sub
End Class
